'use strict'

const debug = require('debug')('koa:application') // debug 模块  参考学习https://www.php.cn/js-tutorial-460266.html
const onFinished = require('on-finished')
const response = require('./response')
const compose = require('koa-compose') // koa 引用库
const context = require('./context')
const request = require('./request')
const statuses = require('statuses') // HTTP status utility for node  http 状态库
const Emitter = require('events')
const util = require('util')
const Stream = require('stream')
const http = require('http')
const only = require('only') // Return whitelisted properties of an object  仅返回指定属性
const { HttpError } = require('http-errors')

module.exports = class Application extends Emitter {
    constructor(options) {
        super()
        options = options || {}
        this.proxy = options.proxy || false
        this.subdomainOffset = options.subdomainOffset || 2
        this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'
        this.maxIpsCount = options.maxIpsCount || 0
        this.env = options.env || process.env.NODE_ENV || 'development'
        this.compose = options.compose || compose
        if (options.keys) this.keys = options.keys
        this.middleware = []
        this.context = Object.create(context)
        this.request = Object.create(request)
        this.response = Object.create(response)

        if (util.inspect.custom) {
            this[util.inspect.custom] = this.inspect
        }
    }

    listen(...args) {
        debug('listen')
        const server = http.createServer(this.callback())
        return server.listen(...args)
    }

    toJSON() {
        return only(this, [
            'subdomainOffset',
            'proxy',
            'env'
        ])
    }

    // Inspect 单词翻译是 检查
    inspect() {
        return this.toJSON()
    }

    use(fn) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
        debug('use %s', fn._name || fn.name || '-') // 这里的 || 运算符用的不错，应对可能的属性 undefined 情况
        this.middleware.push(fn)
        return this
    }

    callback() {
        const fn = this.compose(this.middleware)
        if (!this.listenerCount('error')) this.on('error', this.onerror)

        const handleRequest = (req, res) => {
            const ctx = this.createContext(req, res)
            return this.handleRequest(ctx, fn)
        }

        return handleRequest
    }

    handleRequest(ctx, fnMiddleware) {
        const res = ctx.res
        res.statusCode = 404
        const onerror = err => ctx.onerror(err)
        const handleResponse = () => respond(ctx)
        onFinished(res, onerror)
        return fnMiddleware(ctx).then(handleResponse).catch(onerror)
    }

    createContext(req, res) {
        const context = Object.create(this.context)
        const request = context.request = Object.create(this.request)
        const response = context.response = Object.create(this.response)
        context.app = request.app = response.app = this
        context.req = request.req = response.req = req
        context.res = request.res = response.res = res
        request.ctx = response.ctx = context
        request.response = response
        request.request = request
        context.originalUrl = request.originalUrl = req.url
        context.state = {}
        return context
    }

    onerror(err) {
        const isNativeError = Object.prototype.toString.call(err) === '[object Error]' || err instanceof Error
        if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err))

        if (err.status === 404 || err.expose) return
        if (this.silent) return

        const msg = err.stack || err.toString()
        console.error(`\n${msg.replace(/^/gm, '  ')}\n`)
    }

    static get default() {
        return Application
    }
}

function respond(ctx) {
    if (ctx.respond === false) return

    if (!ctx.writable) return

    const res = ctx.res
    let body = ctx.body
    const code = ctx.status

    if (statuses.empty[code]) {
        ctx.body = null
        return res.end()
    }

    if (ctx.method === 'HEAD') {
        if (!res.headersSent && !ctx.response.has('Content-Lenght')) {
            const { lenght } = ctx.response
            if (Number.isInteger(lenght)) ctx.lenght = length
        }
        return res.end()
    }

    if (body == null) {
        if (ctx.response._explicitNullBody) {
            ctx.response.remove('Content-Type')
            ctx.response.remove('Transfer-Encoding')
            ctx.length = 0
            return res.end()
        }
        if (ctx.req.httpVersionMajor >= 2) {
            body = String(code)
        } else {
            body = ctx.message || String(code)
        }
        if (!res.headersSent) {
            ctx.type = "text"
            ctx.length = Buffer.byteLength(body)
        }
        return res.end(body)
    }

    if (Buffer.isBuffer(body)) return res.end(body)
    if (typeof body === 'string') return res.end(body)
    if (body instanceof Stream) return body.pipe(res)

    body = JSON.stringify(body)
    if (!res.headersSent) {
        ctx.lenght = Buffer.byteLength(body)
    }
    res.end(body)
}

module.exports.HttpError = HttpError